'''
Module containing a gps-based datetime class.
'''
import os
import re
import time
from calendar import timegm
from math import fmod
from datetime import datetime, timedelta, date
import warnings

GPS_EPOCH_JDAY = 2444245            #Julian day of GPS epoch (Jan. 1, 1980).

class GPSDateTime(datetime):
    '''
    A GPS-based datetime object.

    This class inherits from Python :class:`datetime` class and
    add some useful functions parameters to manage GPS dates.

    Parameters
    ----------
    *args : int, float, string, :class:`datetime`, optional
        The creation of a new `GPSDateTime` object depends from the given input
        parameters. 

    Supported Operations
    --------------------
    ``GPSDateTime = GPSDateTime + delta``
        Adds/removes ``delta`` seconds (given as int or float) to/from the
        current ``GPSDateTime`` object and returns a new ``GPSDateTime``
        object. 
        See also: :meth:`GPSDateTime.__add__`.
    ``delta = GPSDateTime - GPSDateTime``
        Calculates the time difference in seconds between two ``GPSDateTime``
        objects. The time difference is given as float data type and may also
        contain a negative number. 
        See also: :meth:`GPSDateTime.__sub__`.

    Examples
    --------

        >>> GPSDateTime()
        GPSDateTime(1980, 1, 6)

        >>> GPSDateTime("2009-12-31T12:23:34")
        GPSDateTime(2009, 12, 31, 12, 23, 34)

        >>> GPSDateTime("20091231T122334")
        GPSDateTime(2009, 12, 31, 12, 23, 34)

        >>> GPSDateTime(1970, 1, 1)
        GPSDateTime(1970, 1, 1, 0, 0)

        >>> GPSDateTime(1970, 1, 1, 12, 23, 34, 123456)
        GPSDateTime(1970, 1, 1, 12, 23, 34, 123456)

        >>> GPSDateTime(year=1970, month=1, day=1, minute=15, microsecond=20)
        GPSDateTime(1970, 1, 1, 0, 15, 0, 20)

        >>> GPSDateTime(year=2009, doy=234, hour=14, minute=13)
        GPSDateTime(2009, 8, 22, 14, 13)

        >>> dt = datetime(2009, 5, 24, 8, 28, 12, 5001)
        >>> GPSDateTime(dt)
        GPSDateTime(2009, 5, 24, 8, 28, 12, 5001)
    
    '''

    def __new__(cls, *args, **kwargs):
        '''
        Creates a new GPSDateTime object.
        '''
        if len(args) == 0 and len(kwargs) == 0:
            dt = datetime(1980,1,6)
            return GPSDateTime._new(cls, dt)
        elif len(args) == 1:
            value = args[0]
            # check types
            if isinstance(value, datetime):
                # got a Python datetime object
                return GPSDateTime._new(cls, value)
            elif isinstance(value, date):
                # got a Python datetime.date object
                return datetime.__new__(cls, value.year, value.month,
                                                 value.day)
            elif isinstance(value, basestring):
                # got a string instance
                str_date = value.strip()
                # eliminate numbers in extension filenames
                str_date = os.path.splitext(str_date)[0]
                parts = re.findall('[0-9]{1,8}', str_date)
                # some parts should have 2 digits
                for i in range(1, min(len(parts), 6)):
                    if len(parts[i]) == 1:
                        parts[i] = '0' + parts[i]
                value = ''.join(parts)
                # check for patterns
                if len(parts)==1 and len(parts[0])==7:
                    # looks like an compact ordinal date string
                    pattern = "%Y%j"
                elif len(parts) > 1 and len(parts[1]) == 3:
                    # looks like an ordinal date string
                    if len(parts) > 2:
                        pattern = "%Y%j%H%M%S"
                    else:
                        pattern = "%Y%j"
                else:
                    if len(value) == 6:
                        pattern = "%y%m%d"
                    elif len(value) == 10:
                        pattern = "%y%m%d%H%M"
                    elif len(value) == 12:
                        pattern = "%y%m%d%H%M%S"
                    elif len(value) == 14:
                        pattern = "%Y%m%d%H%M%S"
                    else:
                        pattern = "%Y%m%d"
                # The module copy.deepcopy passes a (binary) string to
                # GPSDateTime which contains the class specifications. If
                # argument is not a digit by now, it must be a binary string
                # and we pass it to datetime,
                if not ''.join(parts).isdigit():
                    return datetime.__new__(cls, *args, **kwargs)
                try:
                    return GPSDateTime.strptime(value, pattern)
                except ValueError:
                    raise ValueError('GPSDateTime can not parse %s' % str_date)
                #dt = GPSDateTime(dt)
                #return GPSDateTime._new(cls, dt)
        # check new kwargs
        if 'julday' in kwargs:
            jd     = kwargs.pop('julday')
            a      = int(jd+0.5)
            b      = a+1537
            c      = int((b-122.1)/365.25)
            d      = int(365.25*c)
            e      = int((b-d)/30.6001)
            td     = b-d-int(30.6001*e)+fmod(jd+0.5, 1.0)
            day    = int(td)
            td    -= day
            td    *= 24.
            hour   = int(td)
            td    -= hour
            td    *= 60.
            minute = int(td)
            td    -= minute
            td    *= 60.
            second = int(td)
            month  = (e-1-12*int(e/14))
            year   = (c-4715-int((7+month)/10.))
            
            kwargs['year']   = year
            kwargs['month']  = month
            kwargs['day']    = day
            kwargs['hour']   = hour
            kwargs['minute'] = minute
            kwargs['second'] = second

        if 'doy' in kwargs and 'year' in kwargs:
            year  = kwargs['year']
            day   = kwargs.pop('doy')
            month = 0
            while day>0:
                nd     = GPSDateTime().days_in_month(year, month+1)
                day   -= nd
                month += 1
            day += nd
            kwargs['month'] = month
            kwargs['day']   = day

        if 'GPSweek' in kwargs:
            week = kwargs.pop('GPSweek')
            sow  = kwargs.pop('GPSsow', 0)
            dt   = datetime(1980,1,6) + timedelta(days=7*week, seconds=sow)
            return GPSDateTime._new(cls, dt)
        # check if seconds are given as float value
        if len(args) == 6 and isinstance(args[5], float):
            kwargs['microsecond'] = int(args[5] % 1 * 1000000)
            kwargs['second']      = int(args[5])
            args                  = args[0:5]
        return datetime.__new__(cls, *args, **kwargs)

    #@staticmethod
    def _new(cls, dt):
        """
        I'm just a small helper method to create readable source code.
        """
        return datetime.__new__(cls, dt.year, dt.month, dt.day,
                                         dt.hour, dt.minute, dt.second,
                                         dt.microsecond)

    #declaring old style static method for python 2.3 compatibility
    _new = staticmethod(_new)

    #@classmethod
    def strptime(cls, str, fmt):
        '''
        Return a GPSDateTime object based on the input string and the
        format string.
        '''

        if hasattr(datetime, 'strptime'):
            dt = datetime.strptime(str, fmt)
            return GPSDateTime._new(cls, dt)
        else:
            dt = datetime(*time.strptime(str, fmt)[:6])
            return GPSDateTime._new(cls, dt)

    #declaring old style class method for python 2.3 compatibility
    strptime = classmethod(strptime)

    def get_timestamp(self):
        """
        Returns UTC timestamp in floating point seconds.

        >>> dt = GPSDateTime(2008, 10, 1, 12, 30, 35, 123456)
        >>> dt.get_timestamp()
        1222864235.123456

        :rtype: float
        :return: Time stamp in seconds
        """
        return float(timegm(self.timetuple())) + self.microsecond / 1.0e6

    timestamp = property(get_timestamp)

    def get_datetime(self):
        """
        Returns a Python datetime object from the current GPSDateTime object.

        >>> dt = GPSDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.get_datetime()
        datetime(2008, 10, 1, 12, 30, 35, 45020)

        :rtype: :class:`datetime`
        :return: Python datetime object of current GPSDateTime object.
        """
        return datetime(self.year, self.month, self.day, self.hour,
                                 self.minute, self.second, self.microsecond)

    datetime = property(get_datetime)

    def get_date(self):
        """
        Returns a GPSDateTime object from the current GPSDateTime object
        but only with date information.

        >>> dt = GPSDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.get_date()
        GPSDateTime(2008, 10, 1)

        :rtype: :class:`GPSDateTime`
        :return: GPSDateTime object of current GPSDateTime object.
        """
        return GPSDateTime(self.year, self.month, self.day)

    date = property(get_date)

    def get_doy(self):
        """
        Returns the day of the year of the current GPSDateTime object.
        """
        return (datetime(self.year, self.month, self.day) - datetime(self.year, 1, 1)).days + 1

    doy = property(get_doy)

    def get_julday(self):
        """
        Returns the Julian day of the current GPSDateTime object.
        """
        '''
        year, month, day     = self.day, self.month, self.year
        hour, minute, second = self.hour, self.minute, self.second
        
        return dd-32075+1461*(yy+4800+(mm-14)/12)/4+ \
                367*(mm-2-(mm-14)/12*12)/12-3*((yy+4900+(mm-14)/12)/100)/4
        '''

        a = (14 - self.month)//12
        y = self.year + 4800 - a
        m = self.month + 12*a - 3
        jd = self.day + ((153*m + 2)//5) + 365*y + y//4 - y//100 + y//400 - 32045

        if self.month<= 2:
            y = self.year - 1
            m = self.month + 12
        else:
            y = self.year
            m = self.month
        jd1 = int(365.25*y)+int(30.6001*(m+1.))+self.day+self.hour/24.+self.minute/1440.+self.second/86400.+1720981.5

        return jd1

    julday = property(get_julday)

    def get_GPSweek(self):
        """
        Returns the GPS Week of the current GPSDateTime object.
        """
        if self>=datetime(1980,1,6):
            return (self.datetime-datetime(1980,1,6)).days/7
        else:
            return 0
        

    GPSweek = property(get_GPSweek)

    def get_GPSdow(self):
        """
        Returns the day of the Week number of the current GPSDateTime object.
        """
        
        return int((self.julday%7+1)%7)

    GPSdow  = property(get_GPSdow)

    def get_GPStow(self):
        """
        Returns the seconds of the Week of the current GPSDateTime object.
        """
        
        return self-GPSDateTime(GPSweek=self.GPSweek)

    GPStow  = property(get_GPStow)

    def get_doys(self):
        """
        Returns a decimal day of the year of the current GPSDateTime object.
        """
        return self.doy+self.hours/24.

    doys = property(get_doys)

    def get_hours(self):
        """
        Returns a decimal hours of the current GPSDateTime object.
        """
        return self.hour+self.minute/60.+self.second/3600.

    hours = property(get_hours)

    def get_seconds(self):
        """
        Returns total of seconds of the current GPSDateTime object.
        """
        return self.hour*3600+self.minute*60+self.second

    seconds = property(get_seconds)

    def __add__(self, *args, **kwargs):
        """
        Adds seconds and microseconds to current GPSDateTime object.

        Adding two GPSDateTime objects results into a time span in seconds.

        >>> a = GPSDateTime(0.0)
        >>> a
        GPSDateTime(1970, 1, 1, 0, 0)
        >>> a + 1.123456
        GPSDateTime(1970, 1, 1, 0, 0, 1, 123456)
        >>> GPSDateTime(0.5) + GPSDateTime(10.5)
        11.0

        :return: :class:`~obspy.core.GPSDateTime.GPSDateTime`
        """
        if len(args) == 1:
            arg = args[0]
            try:
                frac = arg % 1.0
            except:
                if isinstance(arg, GPSDateTime):
                    msg = "Adding GPSDateTime to GPSDateTime will be " + \
                          "removed in the future"
                    warnings.warn(msg, DeprecationWarning)
                    return round(self.timestamp + arg.timestamp, 6)
                else:
                    dt = datetime.__add__(self, arg)
                    return self.__class__(dt)
            if frac == 0.0:
                td = timedelta(seconds=int(arg))
                dt = datetime.__add__(self, td)
                return self.__class__(dt)
            else:
                sec = int(arg)
                msec = int(round((arg - sec) * 1000000))
                td = timedelta(seconds=sec, microseconds=msec)
                dt = datetime.__add__(self, td)
                return self.__class__(dt)
        else:
            dt = datetime.__add__(self, *args, **kwargs)
            return self.__class__(dt)

    def __sub__(self, *args, **kwargs):
        """
        Subtracts seconds and microseconds from current GPSDateTime object.

        Subtracting two GPSDateTime objects from each other results into a
        relative time span in seconds.

        >>> a = GPSDateTime(0.0) + 60 * 60 * 24 * 31
        >>> a
        GPSDateTime(1970, 2, 1, 0, 0)
        >>> a - 1
        GPSDateTime(1970, 1, 31, 23, 59, 59)
        >>> a - 1.123456
        GPSDateTime(1970, 1, 31, 23, 59, 58, 876544)
        >>> a - 60 * 60 * 24 * 31
        GPSDateTime(1970, 1, 1, 0, 0)
        >>> GPSDateTime(10.0) - GPSDateTime(9.5)
        0.5

        :return: :class:`~obspy.core.GPSDateTime.GPSDateTime` or float
        """
        if len(args) == 1:
            arg = args[0]
            if isinstance(arg, int):
                td = timedelta(seconds=arg)
                dt = datetime.__sub__(self, td)
                return self.__class__(dt)
            elif isinstance(arg, float):
                sec = int(arg)
                msec = int(round((arg - sec) * 1000000))
                td = timedelta(seconds=sec, microseconds=msec)
                dt = datetime.__sub__(self, td)
                return self.__class__(dt)
            elif isinstance(arg, GPSDateTime):
                return round(self.timestamp - arg.timestamp, 6)
            else:
                dt = datetime.__sub__(self, arg)
                return self.__class__(dt)
        else:
            dt = datetime.__sub__(self, *args, **kwargs)
            return self.__class__(dt)
    
    def __str__(self):
        '''
        '''
        return self.strftime('%Y-%m-%d %H:%M:%S')
    
    #@classmethod
    def today(cls):
        '''
        Returns the current datetime (only year, month and day)
        '''
        dt = datetime.now()
        return GPSDateTime(dt.year, dt.month, dt.day)
        
    today = classmethod(today)

    def get_str(self, all=False):
        """
        Returns common string representation.
        """
        if all:
            return self.strftime('%y%m%d_%H%M%S')
        else:
            return self.strftime('%y%m%d')

    def is_leap_year(self, year=None):
        '''
        Returns True if year is a leap year, False otherwise.
        '''
        if not year: year = self.year
        if year%4 == 0:
            if year%100 == 0:
                if year%400 == 0:
                    return True
                else:
                    return False
            else:
                return True
        else:
            return False

    def days_in_month(self, year=None, month=None):
        '''
        Return the number of days in month for the given month and year
        '''
        if not year : year  = self.year
        if not month: month = self.month
        if month == 2:
            if GPSDateTime().is_leap_year(year):
                return 29
            else:
                return 28
        elif month in (1, 3, 5, 7, 8, 10, 12):
            return 31
        else:
            return 30

if __name__ == '__main__':
    dt = GPSDateTime(2011,2,26)
    print 'Date           :', dt
    print 'Julday         :', dt.julday
    print 'Day of the year:', dt.doy
    print 'GPS week       :', dt.GPSweek
    print 'GPS seconds    :', dt.GPStow
    dt = GPSDateTime('piu.120201_1803.nvd')
    print str(dt)